'use strict'
const fs = require('fs')

class ReadByLine {
  constructor(file, options) {
    options = options || {}
    if (!options.readChunk) options.readChunk = 1024
    if (!options.newLineCharacter) {
      options.newLineCharacter = 0x0a // linux line ending
    } else {
      options.newLineCharacter = options.newLineCharacter.charCodeAt(0)
    }
    if (typeof file === 'number') {
      this.fd = file
    } else {
      this.fd = fs.openSync(file, 'r')
    }

    this.options = options
    this.newLineCharacter = options.newLineCharacter
    this.reset()
  }

  _searchInBuffer(buffer, hexNeedle) {
    let found = -1
    for (let i = 0; i <= buffer.length; i++) {
      let b_byte = buffer[i]
      if (b_byte === hexNeedle) {
        found = i
        break
      }
    }
    return found
  }

  reset() {
    this.eofReached = false
    this.linesCache = []
    this.fdPosition = 0
  }

  close() {
    fs.closeSync(this.fd)
    this.fd = null
  }

  _extractLines(buffer) {
    let line
    const lines = []
    let bufferPosition = 0
    let lastNewLineBufferPosition = 0
    // eslint-disable-next-line no-constant-condition
    while (true) {
      let bufferPositionValue = buffer[bufferPosition++]
      if (bufferPositionValue === this.newLineCharacter) {
        line = buffer.slice(lastNewLineBufferPosition, bufferPosition)
        lines.push(line)
        lastNewLineBufferPosition = bufferPosition
      } else if (bufferPositionValue === undefined) {
        break
      }
    }
    let leftovers = buffer.slice(lastNewLineBufferPosition, bufferPosition)
    if (leftovers.length) {
      lines.push(leftovers)
    }
    return lines
  }

  _readChunk(lineLeftovers) {
    let totalBytesRead = 0
    let bytesRead
    const buffers = []
    do {
      const readBuffer = Buffer.allocUnsafe(this.options.readChunk)
      bytesRead = fs.readSync(this.fd, readBuffer, 0, this.options.readChunk, this.fdPosition)
      totalBytesRead = totalBytesRead + bytesRead
      this.fdPosition = this.fdPosition + bytesRead
      buffers.push(readBuffer)
    } while (bytesRead && this._searchInBuffer(buffers[buffers.length - 1], this.options.newLineCharacter) === -1)
    let bufferData = Buffer.concat(buffers)
    if (bytesRead < this.options.readChunk) {
      this.eofReached = true
      bufferData = bufferData.slice(0, totalBytesRead)
    }
    if (totalBytesRead) {
      this.linesCache = this._extractLines(bufferData)
      if (lineLeftovers) {
        this.linesCache[0] = Buffer.concat([lineLeftovers, this.linesCache[0]])
      }
    }
    return totalBytesRead
  }

  next() {
    if (!this.fd) return false
    let line = false
    if (this.eofReached && this.linesCache.length === 0) {
      return line
    }
    let bytesRead
    if (!this.linesCache.length) {
      bytesRead = this._readChunk()
    }
    if (this.linesCache.length) {
      line = this.linesCache.shift()
      const lastLineCharacter = line[line.length - 1]
      if (lastLineCharacter !== this.newLineCharacter) {
        bytesRead = this._readChunk(line)
        if (bytesRead) {
          line = this.linesCache.shift()
        }
      }
    }
    if (this.eofReached && this.linesCache.length === 0) {
      this.close()
    }
    if (line) {
      line = line || []
      if (line[line.length - 1] === this.newLineCharacter) {
        line = line.slice(0, line.length - 1)
      }
    }
    return line.toString()
  }
}

module.exports = ReadByLine
